﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.DirectoryServices;
using System.DirectoryServices.ActiveDirectory;
using System.Linq;
using System.Text;
using ActiveDs;

namespace Library
{
  /// <summary>
  /// Directory にアクセスするための静的メソッド・プロパティを提供するクラスです。
  /// </summary>
  public static class DirectoryAccess
  {
    #region プライベートフィールド
    private static ReadOnlyCollection<string> _categoryNames;   //ディレクトリ オブジェクトの種類の名前リスト
    private static string _domainName, _ntPath;   //ドメイン名, WinNTプロバイダのパス
    private static bool _domLogon, _canConnect;   //ドメインにログオンしているかどうか, ドメインに接続できるかどうか
    private static Dictionary<int, string> _groupTokens;  //ドメイングループのプライマリ グループ トークンと名前の組のリスト
    #endregion

    #region パブリックプロパティ
    /// <summary>
    /// ディレクトリ オブジェクトの種類の名前リストを取得します。
    /// </summary>
    public static ReadOnlyCollection<string> CategoryNames
    {
      get
      {
        if (_categoryNames == null)
        {
          _categoryNames = new ReadOnlyCollection<string>(
            new string[] { "ユーザー", "グループ", "コンピューター", 
              "組織単位", "プリンター", "共有フォルダー", "外部のセキュリティ プリンシパル" });
        }
        return _categoryNames;
      }
    }

    /// <summary>
    /// ドメインに接続できるかどうかを取得します。
    /// </summary>
    public static bool CanConnectDomain
    {
      get
      {
        if (_domainName == null)
        {
          SetData();    //データを設定
        }
        return _canConnect;
      }
    }

    /// <summary>
    /// Active Directory ドメイン名を取得します。
    /// </summary>
    public static string DomainName
    {
      get
      {
        if (_domainName == null)
        {
          SetData();    //データを設定
        }
        return _domainName;
      }
    }

    /// <summary>
    /// ドメイングループのプライマリ グループ トークンと名前の組のリストを取得します。
    /// このプロパティはドメインのグループを取得するメソッドを呼び出すたびに設定されます。
    /// </summary>
    public static IDictionary<int, string> GroupTokens
    {
      get
      {
        return _groupTokens;
      }
    }

    /// <summary>
    /// アプリケーションを実行しているユーザーがドメインにログオンしているかどうかを取得します。
    /// </summary>
    public static bool IsLogonDomain
    {
      get
      {
        if (_domainName == null)
        {
          SetData();    //データを設定
        }
        return _domLogon;
      }
    }
    #endregion

    #region パブリックメソッド
    /// <summary>
    /// 指定した Directory オブジェクトの使用されているリソースを解放します。
    /// </summary>
    /// <param name="items"><see cref="DirectoryObject"/> のコレクション。</param>
    /// <exception cref="ArgumentNullException"><paramref name="items"/> が null です。</exception>
    public static void DisposeItems(IEnumerable<IDirectory> items)
    {
      if (items == null)
      {
        throw new ArgumentNullException("items", "items が null です。");
      }

      foreach (var item in items)
      {
        item.Dispose();
      }
    }

    /// <summary>
    /// ユーザーの所属するグループを取得します。
    /// </summary>
    /// <param name="user">ユーザー。</param>
    /// <returns>所属するグループのコレクション。</returns>
    /// <exception cref="ArgumentNullException"><paramref name="user"/> が null です。</exception>
    public static ReadOnlyCollection<string> GetBelongGroups(IUser user)
    {
      if (user == null)
      {
        throw new ArgumentNullException("user", "user が null です。");
      }

      var groups = new List<string>();
      foreach (IADs group in user.Native.Groups())  //所属するグループ数分(プライマリ グループ以外)
      {
        groups.Add(group.Get("name").ToString());
      }
      if (user is DomainUser)   //ドメインユーザの時
      {
        var primaryGroupId = ((DomainUser)user).PrimaryGroupId;
        groups.Add(GroupTokens[primaryGroupId]);
      }
      groups.Sort();
      return groups.AsReadOnly();
    }

    /// <summary>
    /// 指定した <see cref="DirectoryEntry"/> の所属パスを取得します。
    /// </summary>
    /// <param name="entry"><see cref="DirectoryEntry"/>。</param>
    /// <returns><paramref name="entry"/> の所属パス。所属していない場合は <see cref="String.Empty"/>。</returns>
    /// <exception cref="ArgumentNullException"><paramref name="entry"/> が null です。</exception>
    public static string GetBelongPath(DirectoryEntry entry)
    {
      if (entry == null)
      {
        throw new ArgumentNullException("entry", "entry が null です。");
      }

      return GetBelongPath(entry.Path);
    }

    /// <summary>
    /// 指定した ADSI オブジェクトの所属パスを取得します。
    /// </summary>
    /// <param name="native">ADSI オブジェクト。</param>
    /// <returns><paramref name="native"/> の所属パス。所属していない場合は <see cref="String.Empty"/>。</returns>
    /// <exception cref="ArgumentNullException"><paramref name="native"/> が null です。</exception>
    public static string GetBelongPath(IADs native)
    {
      if (native == null)
      {
        throw new ArgumentNullException("native", "native が null です。");
      }

      return GetBelongPath(native.ADsPath);
    }

    /// <summary>
    /// 指定した LDAP パスの所属パスを取得します。
    /// </summary>
    /// <param name="ldapPath">LDAP パス。</param>
    /// <returns><paramref name="ldapPath"/> の所属パス。所属していない場合は <see cref="String.Empty"/>。</returns>
    public static string GetBelongPath(string ldapPath)
    {
      if (ldapPath == null)
      {
        return String.Empty;
      }

      if ((ldapPath.Contains("OU=") || ldapPath.Contains(",CN=")) == false)   //ドメイン直下の時
      {
        //OUにある場合は・・・LDAP://ドメイン名/CN=○○,OU=OU名,～
        //コンテナにある場合は・・・LDAP://ドメイン名/CN=○○,CN=コンテナ名,～
        return String.Empty;
      }

      var spos = ldapPath.LastIndexOf("CN=");   //開始位置
      if (spos < 0)   //OUの時
      {
        spos = ldapPath.IndexOf("OU=");
      }
      var epos = ldapPath.IndexOf(",DC=");      //終了位置
      var paths = ldapPath.Substring(spos, epos - spos).Split(',');   //"CN=○○"と"OU=○○"部分の配列

      Array.Reverse(paths);
      if (ldapPath.Contains("OU="))   //OUがある時
      {
        if (paths.Length == 1)   //ルートOU(自身)の時・・・LDAP://ドメイン名/OU=○○,DC=～
        {
          return String.Empty;
        }
        else    //ルートOU以外の時
        {
          Array.Resize<string>(ref paths, paths.Length - 1);  //自分の名前を除外
        }
      }

      var sb = new StringBuilder();
      foreach (var ou in paths)
      {
        sb.AppendFormat("{0}/", ou.Substring(3));
      }
      sb.Length--;
      return sb.ToString();
    }

    /// <summary>
    /// コンピューターを取得します。
    /// </summary>
    /// <returns>コンピューターのコレクション。</returns>
    public static IList<Computer> GetComputers()
    {
      var computers = new List<Computer>();
      if (CanConnectDomain)   //ドメインに接続できる時
      {
        using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
        {
          var filter = String.Format("(objectCategory={0})", CategoryType.Computer);
          using (var searcher = new DirectorySearcher(root, filter))
          {
            using (var results = searcher.FindAll())
            {
              foreach (SearchResult res in results)
              {
                computers.Add((Computer)CreateInstance(res.GetDirectoryEntry()));
              }
            }
          }
        }
      }
      return computers.OrderBy(pc => pc.ToString()).ToList();
    }

    /// <summary>
    /// ドメインを取得します。取得できなかった場合、ドメインにログオンしていない場合は null が返されます。
    /// ドメインにログオンしている場合は例外がスローされます。
    /// </summary>
    /// <returns>ドメイン。</returns>
    public static Domain GetDomain()
    {
      try
      {
        _canConnect = true;
        //return Domain.GetCurrentDomain();
        return Domain.GetComputerDomain();  //Webアプリを考慮するならこの方がいいかも
        //var context = new DirectoryContext(DirectoryContextType.Domain);
        //return Domain.GetDomain(context);
      }
      catch
      {
        _canConnect = false;
        if (_domLogon)
        {
          throw;
        }
        return null;
      }
    }

    /// <summary>
    /// グループを取得します。<see cref="GroupTokens"/> プロパティが設定されます。
    /// </summary>
    /// <typeparam name="T">Group オブジェクトの型。</typeparam>
    /// <returns>グループのコレクション。</returns>
    public static IList<T> GetGroups<T>() where T : DirectoryObject, IGroup
    {
      var groups = new List<T>();
      using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
      {
        if (CanConnectDomain)   //ドメインに接続できる時
        {
          var filter = String.Format("(objectCategory={0})", CategoryType.Group);
          GroupTokens.Clear();
          using (var searcher = new DirectorySearcher(root, filter))
          {
            using (var results = searcher.FindAll())
            {
              foreach (SearchResult res in results)
              {
                var group = CreateInstance(res.GetDirectoryEntry());
                groups.Add((T)group);
                GroupTokens.Add(((DomainGroup)group).Token, group.Name);  //プライマリ グループ トークンを追加
              }
            }
          }
        }
        else  //ドメインに接続できない時
        {
          root.Children.SchemaFilter.Add(CategoryType.Group.ToString());
          foreach (DirectoryEntry entry in root.Children)
          {
            groups.Add((T)CreateInstance(entry));
          }
        }
      }
      return groups.OrderBy(group => group.ToString()).ToList();
    }

    /// <summary>
    /// 指定したドメイングループのメンバーを取得します。
    /// </summary>
    /// <param name="group">グループ。</param>
    /// <returns><paramref name="group"/> のメンバー。</returns>
    public static IList<DomainObject> GetMembers(DomainGroup group)
    {
      if (group == null)
      {
        throw new ArgumentNullException("group", "group が null です。");
      }
      if (CanConnectDomain == false)  //ドメインに接続できない時
      {
        return new List<DomainObject>();
      }

      var objects = new List<DomainObject>();
      using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
      {
        //このグループのメンバーを検索
        var filter = String.Format("(memberOf={0})", group.Entry.Properties["distinguishedName"].Value);
        using (var searcher = new DirectorySearcher(root, filter))
        {
          using (var results = searcher.FindAll())
          {
            foreach (SearchResult res in results)
            {
              objects.Add((DomainObject)CreateInstance(res.GetDirectoryEntry()));
            }
          }
          //このグループをプライマリ グループとしているメンバーを検索
          searcher.Filter = String.Format("(&(|(objectCategory={0})(objectCategory={1}))(primaryGroupID={2}))",
            CategoryType.User, CategoryType.Computer, group.Token);
          using (var results = searcher.FindAll())
          {
            foreach (SearchResult res in results)
            {
              objects.Add((DomainObject)CreateInstance(res.GetDirectoryEntry()));
            }
          }
        }
      }
      return objects.OrderBy(o => o.ToString()).ToList();
    }

    /// <summary>
    /// 組織単位（OU）を取得します。
    /// </summary>
    /// <returns>組織単位（OU）のコレクション。</returns>
    public static IList<OrganizationalUnit> GetOrganizationalUnits()
    {
      var ous = new List<OrganizationalUnit>();
      if (CanConnectDomain)   //ドメインに接続できる時
      {
        using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
        {
          var filter = String.Format("(objectCategory={0})", CategoryType.OrganizationalUnit);
          using (var searcher = new DirectorySearcher(root, filter))
          {
            using (var results = searcher.FindAll())
            {
              foreach (SearchResult res in results)
              {
                ous.Add((OrganizationalUnit)CreateInstance(res.GetDirectoryEntry()));
              }
            }
          }
        }
      }
      return ous;
    }

    /// <summary>
    /// 指定したプライマリ グループ トークンを持つドメイングループをプライマリ グループとしているメンバーの
    /// <see cref="DirectoryEntry"/> のコレクションを取得します。
    /// </summary>
    /// <param name="primaryGroupToken">グループのプライマリ グループ トークン。</param>
    /// <returns><paramref name="primaryGroupToken"/> のグループをプライマリ グループとしているメンバーの
    /// <see cref="DirectoryEntry"/> のコレクション。</returns>
    public static IList<DirectoryEntry> GetPrimaryGroupMemberEntries(int primaryGroupToken)
    {
      if (CanConnectDomain == false)  //ドメインに接続できない時
      {
        return new List<DirectoryEntry>();
      }

      var entries = new List<DirectoryEntry>();
      using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
      {
        var filter = String.Format("(&(|(objectCategory={0})(objectCategory={1}))(primaryGroupID={2}))",
          CategoryType.User, CategoryType.Computer, primaryGroupToken);
        using (var searcher = new DirectorySearcher(root, filter))
        {
          using (var results = searcher.FindAll())
          {
            foreach (SearchResult res in results)
            {
              entries.Add(res.GetDirectoryEntry());
            }
          }
        }
      }
      return entries;
    }

    /// <summary>
    /// プリンターを取得します。
    /// </summary>
    /// <returns>プリンターのコレクション。</returns>
    public static IList<PrintQueue> GetPrintQueues()
    {
      var printers = new List<PrintQueue>();
      if (CanConnectDomain)   //ドメインに接続できる時
      {
        using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
        {
          var filter = String.Format("(objectCategory={0})", CategoryType.PrintQueue);
          using (var searcher = new DirectorySearcher(root, filter))
          {
            using (var results = searcher.FindAll())
            {
              foreach (SearchResult res in results)
              {
                printers.Add((PrintQueue)CreateInstance(res.GetDirectoryEntry()));
              }
            }
          }
        }
      }
      return printers.OrderBy(printer => printer.ToString()).ToList();
    }

    /// <summary>
    /// ユーザーを取得します。
    /// </summary>
    /// <typeparam name="T">User オブジェクトの型。</typeparam>
    /// <returns>ユーザーのコレクション。</returns>
    public static IList<T> GetUsers<T>() where T : DirectoryObject, IUser
    {
      var users = new List<T>();
      using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
      {
        if (CanConnectDomain)   //ドメインに接続できる時
        {
          var filter = String.Format("(objectCategory={0})", CategoryType.User);
          using (var searcher = new DirectorySearcher(root, filter))
          {
            using (var results = searcher.FindAll())
            {
              foreach (SearchResult res in results)
              {
                users.Add((T)CreateInstance(res.GetDirectoryEntry()));
              }
            }

            if (GroupTokens.Count == 0)   //ドメイングループのプライマリ グループ トークンと名前の組のリストがない時
            {
              LoadGroupTokens(searcher);  //ドメイングループのプライマリ グループ トークンと名前の組のリストをロード
            }
          }
        }
        else  //ドメインに接続できない時
        {
          root.Children.SchemaFilter.Add(CategoryType.User.ToString());
          foreach (DirectoryEntry entry in root.Children)
          {
            users.Add((T)CreateInstance(entry));
          }
        }
      }
      return users.OrderBy(user => user.ToString()).ToList();
    }

    /// <summary>
    /// 共有フォルダーを取得します。
    /// </summary>
    /// <returns>共有フォルダーのコレクション。</returns>
    public static IList<Volume> GetVolumes()
    {
      var folders = new List<Volume>();
      if (CanConnectDomain)   //ドメインに接続できる時
      {
        using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
        {
          var filter = String.Format("(objectCategory={0})", CategoryType.Volume);
          using (var searcher = new DirectorySearcher(root, filter))
          {
            using (var results = searcher.FindAll())
            {
              foreach (SearchResult res in results)
              {
                folders.Add((Volume)CreateInstance(res.GetDirectoryEntry()));
              }
            }
          }
        }
      }
      return folders.OrderBy(folder => folder.ToString()).ToList();
    }

    /// <summary>
    /// 指定した LDAP パスの名前(オブジェクト名)を取得します。
    /// </summary>
    /// <param name="ldapPath">LDAP パス。</param>
    /// <returns>LDAP パスの名前(オブジェクト名)。<paramref name="ldapPath"/> が LDAP パスでない場合は <paramref name="ldapPath"/>。</returns>
    /// <exception cref="ArgumentNullException"><paramref name="ldapPath"/> が null です。</exception>
    public static string PathToCn(string ldapPath)
    {
      if (ldapPath == null)
      {
        throw new ArgumentNullException("ldapPath", "ldapPath が null です。");
      }

      var spos = ldapPath.IndexOf('=') + 1;
      if (spos == 0)
      {
        return ldapPath;
      }

      var epos = ldapPath.IndexOf(',');
      if (epos > 0)
      {
        return ldapPath.Substring(spos, epos - spos);
      }
      else
      {
        return ldapPath.Substring(spos);
      }
    }
    #endregion

    #region インターナルメソッド
    /// <summary>
    /// 指定した名前と種類の Directory オブジェクトを検索します。
    /// </summary>
    /// <param name="name">検索する名前。</param>
    /// <param name="objectCategory">Directory オブジェクトの種類。</param>
    /// <returns>見つかった場合はそのオブジェクト。見つからなかった場合は null。</returns>
    /// <exception cref="ArgumentNullException"><paramref name="name"/> が null です。</exception>
    /// <exception cref="InvalidEnumArgumentException"><paramref name="objectCategory"/> が有効な <see cref="CategoryType"/> ではありません。</exception>
    internal static DirectoryObject FindDirectoryObject(string name, CategoryType objectCategory)
    {
      if (name == null)
      {
        throw new ArgumentNullException("name", "name が null です。");
      }
      if (IsValidCategoryType(objectCategory) == false)
      {
        throw new InvalidEnumArgumentException("objectCategory が有効な CategoryType ではありません。");
      }

      using (var root = GetRootEntry())   //ルートのDirectoryEntryを取得
      {
        if (CanConnectDomain)   //ドメインに接続できる時
        {
          string filter;
          switch (objectCategory)
          {
            case CategoryType.User:
              filter = String.Format("(&(objectCategory={0})(sAMAccountName={1}))", objectCategory, name);
              break;
            case CategoryType.OrganizationalUnit:
              filter = String.Format("(&(objectCategory={0})(distinguishedName={1}))", objectCategory, name);
              break;
            case CategoryType.PrintQueue:
              filter = String.Format("(&(objectCategory={0})(printerName={1}))", objectCategory, name);
              break;
            default:
              filter = String.Format("(&(objectCategory={0})(name={1}))", objectCategory, name);
              break;
          }
          using (var searcher = new DirectorySearcher(root, filter))
          {
            var result = searcher.FindOne();
            return (result == null) ? null : CreateInstance(result.GetDirectoryEntry());
          }
        }
        else  //ドメインに接続できない時
        {
          return CreateInstance(root.Children.Find(name, objectCategory.ToString()));
        }
      }
    }

    /// <summary>
    /// 指定した組織単位直下のオブジェクトのリストを取得します。
    /// </summary>
    /// <param name="ou">組織単位。</param>
    /// <returns><paramref name="ou"/> 直下のオブジェクトのコレクション。</returns>
    internal static List<DomainObject> GetStoredDomainObjects(OrganizationalUnit ou)
    {
      var objects = new List<DomainObject>();
      using (var searcher = new DirectorySearcher(ou.Entry))
      {
        searcher.SearchScope = SearchScope.OneLevel;
        using (var results = searcher.FindAll())
        {
          foreach (SearchResult res in results)
          {
            objects.Add((DomainObject)CreateInstance(res.GetDirectoryEntry()));
          }
        }
      }
      return objects.OrderBy(o => o.Name).ToList();
    }
    #endregion

    #region プライベートメソッド
    /// <summary>
    /// <see cref="DirectoryObject"/> のインスタンスを作成します。
    /// </summary>
    /// <param name="entry"><see cref="DirectoryEntry"/>。</param>
    /// <returns><paramref name="entry"/> の <see cref="DirectoryObject"/> のインスタンス。</returns>
    /// <exception cref="ArgumentException"><paramref name="entry"/> の種類が <see cref="CategoryType"/> に該当しません。</exception>
    private static DirectoryObject CreateInstance(DirectoryEntry entry)
    {
      CategoryType category;
      if (Enum.TryParse<CategoryType>(entry.SchemaClassName, true, out category) == false)
      {
        throw new ArgumentException("entry の種類が CategoryType に該当しません。", "entry");
      }

      switch (category)
      {
        case CategoryType.User:
          if (CanConnectDomain)   //ドメインに接続できる時
          {
            return new DomainUser(entry);
          }
          else    //ドメインに接続できない時
          {
            return new LocalUser(entry);
          }
        case CategoryType.Group:
          if (CanConnectDomain)   //ドメインに接続できる時
          {
            return new DomainGroup(entry);
          }
          else    //ドメインに接続できない時
          {
            return new LocalGroup(entry);
          }
        case CategoryType.Computer:
          return new Computer(entry);
        case CategoryType.OrganizationalUnit:
          return new OrganizationalUnit(entry);
        case CategoryType.PrintQueue:
          return new PrintQueue(entry);
        case CategoryType.Volume:
          return new Volume(entry);
        default:
          return new ForeignSecurityPrincipal(entry);
      }
    }

    /// <summary>
    /// ルートの <see cref="DirectoryEntry"/> を取得します。
    /// </summary>
    /// <returns>ルートの <see cref="DirectoryEntry"/>。</returns>
    private static DirectoryEntry GetRootEntry()
    {
      if (CanConnectDomain)   //ドメインに接続できる時
      {
        using (var dom = GetDomain())   //ドメインを取得
        {
          return dom.GetDirectoryEntry();
        }
      }
      else  //ドメインに接続できない時
      {
        return new DirectoryEntry(_ntPath);
      }
    }

    /// <summary>
    /// Directory オブジェクトの種類を検証します。
    /// </summary>
    /// <param name="category">Directory オブジェクトの種類。</param>
    /// <returns>Directory オブジェクトの種類が有効な場合は true。それ以外の場合は false。</returns>
    private static bool IsValidCategoryType(CategoryType category)
    {
      switch (category)
      {
        case CategoryType.User:
        case CategoryType.Group:
        case CategoryType.Computer:
        case CategoryType.OrganizationalUnit:
        case CategoryType.PrintQueue:
        case CategoryType.Volume:
        case CategoryType.ForeignSecurityPrincipal:
          return true;
        default:
          return false;
      }
    }

    /// <summary>
    /// ドメイングループのプライマリ グループ トークンと名前の組のリストをロードします。
    /// </summary>
    /// <param name="searcher"><see cref="DirectorySearcher"/>。</param>
    private static void LoadGroupTokens(DirectorySearcher searcher)
    {
      searcher.Filter = String.Format("(objectCategory={0})", CategoryType.Group);
      using (var results = searcher.FindAll())
      {
        foreach (SearchResult res in results)
        {
          var group = (DomainGroup)CreateInstance(res.GetDirectoryEntry());
          GroupTokens.Add(group.Token, group.Name);   //プライマリ グループ トークンを追加
        }
      }
    }

    /// <summary>
    /// データを設定します。
    /// </summary>
    private static void SetData()
    {
      _domainName = String.Empty;
      _ntPath = String.Empty;
      _domLogon = (String.Compare(Environment.MachineName, Environment.UserDomainName, true) != 0);
      _groupTokens = new Dictionary<int, string>();

      using (var domain = GetDomain())    //ドメインを取得
      {
        if (domain != null)   //取得できた時
        {
          _domainName = domain.Name;
          return;
        }
      }
      _ntPath = String.Format("WinNT://{0}", Environment.MachineName);
    }
    #endregion
  }
}
